import type {IncomingRequestCfProperties, ExecutionContext } from "@cloudflare/workers-types";
import { Env, DRequest } from "./sideload";

export type Handler = (
	request:DRequest<unknown, IncomingRequestCfProperties<unknown>>,
	env: Env,
	ctx: ExecutionContext
) => Promise<Response>;

export type ErrorHandler = (
	request:DRequest<unknown, IncomingRequestCfProperties<unknown>>,
	env: Env,
	ctx: ExecutionContext,
	error: number | unknown | null
) => Promise<Response>;

export type Arguments = {
	[name: string]: string;
};

export type ArgumentsExtractor = (realpath: string) => Arguments;

export function resolvePath(path: string): [RegExp, ArgumentsExtractor] {
	if (path == '/')
		return [
			/^\/$/,
			(realpath: string) => {
				return {};
			},
		];

	const regExpParts: string[] = [];
	const argumentsTable: {
		[index: number]: string;
	} = {};

	path.split('/').forEach((segment, index: number) => {
		if (segment.startsWith(':')) {
			regExpParts.push('[^\\/]+');
			argumentsTable[index] = segment.slice(1);
			return;
		}
		regExpParts.push(segment);
	});
	if (regExpParts[regExpParts.length - 1] == '') {
		regExpParts.pop();
	}

	const regExp = new RegExp("^" + regExpParts.join('\\/') + '(\\/)?' + "$");
	const extractor: ArgumentsExtractor = (realpath: string): Arguments => {
		const args: Arguments = {};

		if (realpath[realpath.length - 1] == '/') realpath = realpath.slice(0, realpath.length - 1);
		realpath.split('/').forEach((segment: string, index: number) => {
			if (!argumentsTable[index]) return;
			args[argumentsTable[index]] = segment;
		});

		return args;
	};

	return [regExp, extractor];
}

export class Router {
	handlers: {
		[method: string]: [RegExp, ArgumentsExtractor, Handler][];
	} = {};

	errorHandlers: {
		[code: number]: ErrorHandler;
	} = {};

	async handle(request:DRequest<unknown, IncomingRequestCfProperties<unknown>>, env: Env, ctx: ExecutionContext): Promise<Response> {
		const url = new URL(request.url);
		if (!this.handlers[request.method.toUpperCase()]) return this.handleError(request, env, ctx, 404, null);

		for (const rule of this.handlers[request.method]) {
			if (!rule[0].test(url.pathname)) continue;
			request.arguments = rule[1](url.pathname);
			try {
				return await rule[2](request, env, ctx);
			} catch (error) {
				if (typeof error == 'number') {
					return this.handleError(request, env, ctx, error, null);
				}
				return this.handleError(request, env, ctx, 500, error);
			}
		}

		return this.handleError(request, env, ctx, 404, null);
	}

	catch(code: number): (handler: ErrorHandler) => ErrorHandler {
		return (handler) => {
			this.errorHandlers[code] = handler;
			return handler;
		};
	}

	async handleError(
		request:DRequest<unknown, IncomingRequestCfProperties<unknown>>,
		env: Env,
		ctx: ExecutionContext,
		code: number,
		error: number | unknown | null
	): Promise<Response> {
		if (!this.errorHandlers[code]) {
			console.warn('no handler found for error ' + code);
			return new Response(code.toString());
		}
		const res = await this.errorHandlers[code](request, env, ctx, error);
		return res;
	}

	route(path: string, methods: string | string[]): (handler: Handler) => Handler {
		if (!Array.isArray(methods)) return this.route(path, [methods]);
		return (handler) => {
			const [regExp, extractor] = resolvePath(path);

			for (const method of methods.map(m=>m.toUpperCase())) {
				this.handlers[method] = this.handlers[method] ?? [];
				this.handlers[method].push([regExp, extractor, handler]);
			}

			return handler;
		};
	}

	get(path: string): (handler: Handler) => Handler {
		return this.route(path, 'get');
	}
	post(path: string): (handler: Handler) => Handler {
		return this.route(path, 'post');
	}
	put(path: string): (handler: Handler) => Handler {
		return this.route(path, 'put');
	}
	delete(path: string): (handler: Handler) => Handler {
		return this.route(path, 'delete');
	}
}
